Skip to content

feat(bridge): session start-queue for extension-created sessions (#1328) - #1330

Closed
suleimansh wants to merge 2 commits into
mainfrom
suleimansh/feat/1328-extension-sessions
Closed

feat(bridge): session start-queue for extension-created sessions (#1328)#1330
suleimansh wants to merge 2 commits into
mainfrom
suleimansh/feat/1328-extension-sessions

Conversation

@suleimansh

Copy link
Copy Markdown
Contributor

Refs #1328. Incomplete on purpose - see "What is missing" before reviewing.

What is here

Daemon half (complete, tested). A start-queue the daemon fills and the extension drains.

  • bridge-starts.ts: validation (owner/name, a git branch name, a bounded prompt), claim-on-read, and a claim that ages out after 90s so a browser quitting mid-creation retries instead of bricking the request.
  • GET /_bridge/start claims the next request as it serves it. A GET that mutates, which is unusual and deliberate: handing the same request to two polling tabs would create two cloud sessions on the user's account.
  • POST /_bridge/started takes back an id, a boolean, an optional session id and a note. Success without a session id is recorded as a failure, because a run pointing nowhere is not a usable outcome.

The bridge's "deliberately tiny input" rule is intact. A start request carries the free text that rule excludes, and it stays excluded because the direction is outward: the daemon produces it, the extension reads it, and what comes back is only an id, a boolean and a session id. Nothing here widens what someone holding the bridge token can say.

Page half (content.js). createSession drives the repo picker, the branch picker, the composer and send, then reads the session id from the URL - the one thing claude.ai is guaranteed to tell us and exactly what the daemon joins runs on. probeNewSession describes the page's controls without touching them.

Every selector in there is a guess about someone else's UI, so the code is built to report rather than insist: each failure names the control it could not find, and the probe exists so the first live run diagnoses instead of just failing.

What is missing

The service-worker loop in background.js - poll /_bridge/start, open the new-session tab, hand the request to the content script, report back. My tooling refused to write it (a permission classifier blocked the edit), so it is deliberately absent rather than forgotten. Without it nothing drains the queue, so this PR changes no runtime behaviour: the routes exist, the queue exists, and nobody fills or empties it.

Also not started, both named in #1328: CloudDriver's extension-backed mode (hand-off resolving on the bridge report), and the concurrency question. On the concurrency point the honest answer from the page half is that creation is inherently serial - the flow navigates one new-session page, so ten sessions are ten sequential creations, a throughput question rather than a correctness one.

The policy question is still open

#1328 records that this direction needs the Usage Policy angle settled deliberately. My reading is that Consumer Terms section 3 does not permit it, and that the "user-installed extension in your own browser" arguments are not carve-outs; the two carve-outs are an API key or explicit permission. That is written up in full for the issue. This PR is the code that exists, not a verdict that the direction is settled.

Checks

Framework suite 1535/0, typecheck clean. Extension harness (node check.mjs) 13/13, unchanged by the content.js additions.

The daemon can queue a repo, branch and prompt; the extension claims it,
drives the claude.ai new-session flow and reports the session it became.

Daemon half:
- bridge-starts.ts: the queue, with validation, claim-on-read and a claim
  that ages out so a browser quitting mid-creation retries.
- GET /_bridge/start and POST /_bridge/started.
- The bridge's input surface is unchanged: the daemon PRODUCES a start
  request, and the extension posts back only an id, a boolean and a
  session id. No free text is ever posted to the daemon.

Page half (content.js):
- createSession drives repo picker, branch picker, composer, send, and
  reads the session id from the URL.
- probeNewSession describes the page's controls without touching them,
  so the first live run diagnoses instead of just failing.

Refs #1328
@suleimansh suleimansh self-assigned this Jul 27, 2026
@suleimansh
suleimansh marked this pull request as ready for review July 27, 2026 22:38
check.mjs gains eight cases driving createSession and probeNewSession
against a synthetic new-session page in jsdom: no browser, no claude.ai,
no session spent.

What they pin is the flow, not the markup: repo and branch are chosen
before the prompt is sent, the session id is read from the URL rather
than from the page, and each failure names the control it could not
find. A send that never becomes a session URL is reported as a failure,
because a run pointing nowhere is worse than a clean refusal.

Also fixes a real bug the fixtures surfaced: the already-set check in
pickFromMenu used contains-matching, so branch 'main' was satisfied by a
repo trigger reading 'acme/domain-tools' and the branch was never picked
at all. Exact match only for that check. Revert-proven by the case named
for it.

Refs #1328
@suleimansh

Copy link
Copy Markdown
Contributor Author

Second commit adds the offline half of the testing story: check.mjs gains a fake new-session page and eight cases driving createSession and probeNewSession in jsdom. No browser, no claude.ai, no session spent - the shape Rom asked for on fake-agent e2e.

What they pin is the flow rather than the markup: repo and branch are chosen before the prompt is sent, the session id is read from the URL rather than from anything on the page, each failure names the control it could not find, and a send that never becomes a session URL is a failure rather than a silent success.

They also caught a real bug. pickFromMenu's already-set check used contains-matching, so branch main was satisfied by a repo trigger reading acme/domain-tools and the branch was never picked at all. Exact match only for that check now; the case named for it is revert-proven (reverting the fix reports branch already set to acme/domain-tools).

Extension harness now 21/21. Unchanged from the first commit: the service-worker loop is still absent, so nothing drains the queue and this PR still changes no runtime behaviour.

CI note: the first run of this commit went red on @gemstack/framework-dashboard#test, which this commit does not touch (it only changes spike/cc-web-extension/, outside the workspace globs). 633/633 locally; one rerun cleared it. Same flake family as the earlier #1122 signatures.

@suleimansh
suleimansh marked this pull request as draft July 30, 2026 21:23
@suleimansh
suleimansh marked this pull request as ready for review August 1, 2026 18:00
@suleimansh
suleimansh marked this pull request as draft August 1, 2026 18:00
@suleimansh
suleimansh force-pushed the suleimansh/feat/1328-extension-sessions branch from 5326baf to 2145d6e Compare August 22, 2026 14:44
suleimansh added a commit that referenced this pull request Aug 24, 2026
…de.ai's repo picker (#1328) (#1693)

* Web runs: the Chrome extension creates the cloud session through claude.ai's repo picker (#1328)

A session created through the page's repository picker is repo-bound and can push and
open its pull request; `claude --cloud` has, on some accounts, produced a bundle upload that
never could (#1320). So a web run now asks its daemon for an extension-created session first.

Daemon: the session start-queue (`bridge-starts.ts`, re-landed from #1330) behind two faces on
the one token — the extension's `GET /_bridge/start` (claim-on-read) + `POST /_bridge/started`,
and the run's `POST /_web-start` + `GET /_web-start/<id>` (`web-start-endpoints.ts`), which
answers 409 at once when no extension has called within three minutes. The daemon hands every
spawned run its own URL in `TF_DAEMON_URL`.

Driver: `CloudDriver` takes `extension: { daemonUrl, token }` (wired by the CLI from the env
and the registry token); after the anchor push and trust check it posts the request — repo from
the GitHub remote, the pushed ref, the whole hand-off prompt — and polls until created, then
reports exactly as the CLI path does. 409/404, no GitHub remote or an unpushed ref fall back
to the CLI's cloud mode with a notice; an extension that tried and failed fails the turn with
its note.

Extension 0.11.0 (daemon lockstep): the worker claims requests on the answer beat, one at a
time, opens claude.ai/code pinned and inactive, and the content script drives the repo chip's
searchable list, verifies the branch chip reads the pushed ref before sending, types the prompt,
sends, and reads the session id off the address. A created session's tab is a watched tab; a
failed attempt's tab is closed and the note says what the page lacked. Harness cases cover
the drive, the wrong-branch refusal and the missing-picker report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Extension: wait for the repo chip or picker to render, and treat a remembered repo's chip as the picker (#1328 dogfood 1)

First live attempt: the page had remembered the last repo picked, so its chips (the-framework / main, as comboboxes)
were already there — but they render after the composer, and createSession read the page before they did, then looked
for a "Select repo" trigger that no longer exists once a repo is remembered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Extension: drive the new-session page as it actually is — combobox chips in order, visible options only, React-safe search (#1328 dogfood 2)

Run 2 clicked the repo chip itself as if it were a list entry (its text is the bare repo name) and then found no
chip "afterwards". Read from the live page: the chips are combobox buttons in the order repo, branch, add; a picker
is a dialog with a role=combobox search input and a listbox of options; a closed picker's options stay in the DOM,
so only visible options are entries, and the trigger is never one. The branch list does offer a ref pushed seconds
earlier (checked live), so verifying the chip before sending is enough.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Extension: wait for the picker's list to load before filtering it, fall back to the whole list, and say what the list showed on failure (#1328 dogfood 3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harness: a list that already holds the entry is not filtered first

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Extension: read chip and entry labels through the page's icon-font glyphs (#1328 dogfood 4)

In the content script's world the labels carry private-use glyphs — the chips read "\ue048the-framework" and
"\ue078main", the chosen entry "framework/the-framework\ue03b" — so no exact match ever fired. The mirror already
strips those; the control labels now do too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@suleimansh

Copy link
Copy Markdown
Contributor Author

Superseded by #1693 (merged): the daemon half re-landed on today's paths, the extension half built and dogfooded.

🤖 curated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant